/*
  A lightweight IndexedDB wrapper with a localForage-like API.
  Methods: ready, getItem, setItem, removeItem, clear, keys, length, iterate, close
*/

export type IndexedDBStoreOptions = {
  dbName?: string;
  storeName?: string;
  version?: number;
  keyPath?: string | null;
  autoIncrement?: boolean;
};

type IDBValue = unknown;

function promisifyRequest<T>(request: IDBRequest<T>): Promise<T> {
  return new Promise<T>((resolve, reject) => {
    request.onsuccess = () => resolve(request.result);
    request.onerror = () => reject(request.error);
  });
}

function transactionDone(tx: IDBTransaction): Promise<void> {
  return new Promise<void>((resolve, reject) => {
    tx.oncomplete = () => resolve();
    tx.onabort = () => reject(tx.error);
    tx.onerror = () => reject(tx.error);
  });
}

export class AsyncIndexedDB {
  private dbName: string;
  private storeName: string;
  private version: number;
  private keyPath: string | null | undefined;
  private autoIncrement: boolean;
  private dbPromise: Promise<IDBDatabase> | null = null;

  constructor(options: IndexedDBStoreOptions = {}) {
    this.dbName = options.dbName ?? 'app-db';
    this.storeName = options.storeName ?? 'keyval';
    this.version = options.version ?? 1;
    this.keyPath = options.keyPath ?? null;
    this.autoIncrement = options.autoIncrement ?? false;
  }

  private openDB(): Promise<IDBDatabase> {
    if (this.dbPromise) return this.dbPromise;

    this.dbPromise = new Promise<IDBDatabase>((resolve, reject) => {
      // First, open the database without specifying a version to attach to the latest.
      const request = indexedDB.open(this.dbName);

      request.onupgradeneeded = () => {
        // This fires only when DB is first created (version 1). Create store if missing.
        const db = request.result;
        if (!db.objectStoreNames.contains(this.storeName)) {
          db.createObjectStore(this.storeName, {
            keyPath: this.keyPath ?? undefined,
            autoIncrement: this.autoIncrement,
          });
        }
      };

      request.onsuccess = () => {
        const db = request.result;
        const targetVersion = this.version ?? db.version;
        const needBumpForStore = !db.objectStoreNames.contains(this.storeName);
        const needBumpForVersion = typeof targetVersion === 'number' && targetVersion > db.version;

        if (needBumpForStore || needBumpForVersion) {
          const nextVersion = needBumpForVersion
            ? targetVersion
            : db.version + 1;
          db.close();
          const upgradeReq = indexedDB.open(this.dbName, nextVersion);
          upgradeReq.onupgradeneeded = () => {
            const udb = upgradeReq.result;
            if (!udb.objectStoreNames.contains(this.storeName)) {
              udb.createObjectStore(this.storeName, {
                keyPath: this.keyPath ?? undefined,
                autoIncrement: this.autoIncrement,
              });
            }
          };
          upgradeReq.onsuccess = () => resolve(upgradeReq.result);
          upgradeReq.onerror = () => reject(upgradeReq.error);
          upgradeReq.onblocked = () => reject(new Error('IndexedDB upgrade request blocked'));
          return;
        }

        resolve(db);
      };
      request.onerror = () => reject(request.error);
      request.onblocked = () => {
        // If blocked, surface as an error to allow callers to handle/retry
        reject(new Error('IndexedDB open request blocked'));
      };
    });

    return this.dbPromise;
  }

  async ready(): Promise<IDBDatabase> {
    return this.openDB();
  }

  private async getStore(mode: IDBTransactionMode): Promise<IDBObjectStore> {
    const db = await this.openDB();
    const tx = db.transaction(this.storeName, mode);
    return tx.objectStore(this.storeName);
  }

  async getItem<T = IDBValue>(key: IDBValidKey): Promise<T | null> {
    const db = await this.openDB();
    const tx = db.transaction(this.storeName, 'readonly');
    const store = tx.objectStore(this.storeName);
    const req = store.get(key);
    const result = await promisifyRequest<T | undefined>(req);
    await transactionDone(tx);
    return (result as T) ?? null;
  }

  async setItem<T = IDBValue>(key: IDBValidKey, value: T): Promise<T> {
    const db = await this.openDB();
    const tx = db.transaction(this.storeName, 'readwrite');
    const store = tx.objectStore(this.storeName);
    const req = store.put(value as unknown as IDBValue, this.keyPath ? undefined : key);
    await promisifyRequest(req as unknown as IDBRequest<unknown>);
    await transactionDone(tx);
    return value;
  }

  async removeItem(key: IDBValidKey): Promise<void> {
    const db = await this.openDB();
    const tx = db.transaction(this.storeName, 'readwrite');
    const store = tx.objectStore(this.storeName);
    const req = store.delete(key);
    await promisifyRequest(req);
    await transactionDone(tx);
  }

  // Remove multiple keys in a single readwrite transaction
  async removeKeys(keys: IDBValidKey[]): Promise<void> {
    if (!Array.isArray(keys) || keys.length === 0) return;
    const db = await this.openDB();
    const tx = db.transaction(this.storeName, 'readwrite');
    const store = tx.objectStore(this.storeName);
    await Promise.all(
      keys.map((k) => promisifyRequest(store.delete(k)))
    );
    await transactionDone(tx);
  }

  async clear(): Promise<void> {
    const db = await this.openDB();
    const tx = db.transaction(this.storeName, 'readwrite');
    const store = tx.objectStore(this.storeName);
    const req = store.clear();
    await promisifyRequest(req);
    await transactionDone(tx);
  }

  async keys(): Promise<string[]> {
    const db = await this.openDB();
    const tx = db.transaction(this.storeName, 'readonly');
    const store = tx.objectStore(this.storeName);

    // Prefer getAllKeys when available
    const anyStore = store as unknown as { getAllKeys?: () => IDBRequest<IDBValidKey[]>; openKeyCursor?: () => IDBRequest<IDBCursor | null> };
    let result: IDBValidKey[];
    if (typeof anyStore.getAllKeys === 'function') {
      result = await promisifyRequest<IDBValidKey[]>(anyStore.getAllKeys());
    } else {
      result = await new Promise<IDBValidKey[]>((resolve, reject) => {
        const keys: IDBValidKey[] = [];
        const cursorReq = (anyStore.openKeyCursor ? anyStore.openKeyCursor() : store.openCursor());
        cursorReq.onsuccess = () => {
          const cursor = cursorReq.result as IDBCursor | null;
          if (!cursor) return resolve(keys);
          keys.push(cursor.primaryKey);
          cursor.continue();
        };
        cursorReq.onerror = () => reject(cursorReq.error);
      });
    }

    await transactionDone(tx);
    return result.map((k) => String(k));
  }

  async length(): Promise<number> {
    const db = await this.openDB();
    const tx = db.transaction(this.storeName, 'readonly');
    const store = tx.objectStore(this.storeName);
    const req = store.count();
    const count = await promisifyRequest<number>(req);
    await transactionDone(tx);
    return count;
  }

  // Get all values in the current object store
  async values<T = IDBValue>(): Promise<T[]> {
    const db = await this.openDB();
    const tx = db.transaction(this.storeName, 'readonly');
    const store = tx.objectStore(this.storeName);

    const anyStore = store as unknown as { getAll?: () => IDBRequest<T[]> };
    if (typeof anyStore.getAll === 'function') {
      const all = await promisifyRequest<T[]>(anyStore.getAll());
      await transactionDone(tx);
      return all ?? [];
    }

    const collected: T[] = [];
    await new Promise<void>((resolve, reject) => {
      const req = store.openCursor();
      req.onsuccess = () => {
        const cursor = req.result as IDBCursorWithValue | null;
        if (!cursor) return resolve();
        collected.push(cursor.value as T);
        cursor.continue();
      };
      req.onerror = () => reject(req.error);
    });
    await transactionDone(tx);
    return collected;
  }

  // Get all entries (key-value pairs) in the current object store
  async entries<T = IDBValue>(): Promise<Array<{ key: string; value: T }>> {
    const db = await this.openDB();
    const tx = db.transaction(this.storeName, 'readonly');
    const store = tx.objectStore(this.storeName);

    const entries: Array<{ key: string; value: T }> = [];
    await new Promise<void>((resolve, reject) => {
      const req = store.openCursor();
      req.onsuccess = () => {
        const cursor = req.result as IDBCursorWithValue | null;
        if (!cursor) return resolve();
        entries.push({ key: String(cursor.primaryKey), value: cursor.value as T });
        cursor.continue();
      };
      req.onerror = () => reject(req.error);
    });
    await transactionDone(tx);
    return entries;
  }

  // Get all data as a Record keyed by stringified key
  async record<T = IDBValue>(): Promise<Record<string, T>> {
    const db = await this.openDB();
    const tx = db.transaction(this.storeName, 'readonly');
    const store = tx.objectStore(this.storeName);

    const obj: Record<string, T> = {};
    await new Promise<void>((resolve, reject) => {
      const req = store.openCursor();
      req.onsuccess = () => {
        const cursor = req.result as IDBCursorWithValue | null;
        if (!cursor) return resolve();
        obj[String(cursor.primaryKey)] = cursor.value as T;
        cursor.continue();
      };
      req.onerror = () => reject(req.error);
    });
    await transactionDone(tx);
    return obj;
  }

  async iterate<T = IDBValue, R = unknown>(
    iterator: (value: T, key: string, index: number) => R | Promise<R>
  ): Promise<R | undefined> {
    const db = await this.openDB();
    const tx = db.transaction(this.storeName, 'readonly');
    const store = tx.objectStore(this.storeName);

    return new Promise<R | undefined>((resolve, reject) => {
      let index = 0;
      const req = store.openCursor();
      req.onsuccess = async () => {
        const cursor = req.result as IDBCursorWithValue | null;
        if (!cursor) {
          resolve(undefined);
          return;
        }
        try {
          const maybe = await iterator(cursor.value as T, String(cursor.primaryKey), index++);
          if (maybe !== undefined) {
            resolve(maybe);
            tx.abort();
            return;
          }
          cursor.continue();
        } catch (e) {
          reject(e);
          tx.abort();
        }
      };
      req.onerror = () => reject(req.error);
    });
  }

  async close(): Promise<void> {
    if (!this.dbPromise) return;
    const db = await this.dbPromise;
    db.close();
    this.dbPromise = null;
  }
}

// Convenience factory to create isolated stores
export function createIDB(options?: IndexedDBStoreOptions): AsyncIndexedDB {
  return new AsyncIndexedDB(options);
}
